<?php
###
#  Baing PHP 框架 1.0.0
#  http://baing.co/
#
#  Copyright: 白晓阳
#  License: CC BY-NC-SA 4.0
#  https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh
###

namespace bai\resource\application;

use bai\feature\RuntimeException, bai\feature\ResourceException, bai\feature\ExtensionException;


/**
 * <PDO>
 * Runtime
 * [
 *     'dsn' => 'dsn'
 *     'username' => 'username'
 *     'password' => 'password'
 * ]
 * [
 *     'query' => 'SQL query'
 *     'params' => []
 * ]
 */
class Database extends \bai\resource\Application
{
    public const START = 'TRANSACTION_START';
    public const CLOSE = 'TRANSACTION_CLOSE';

    private const EXTENSION = 'PDO';

    protected static function setupId(array &$runtime, string $key = ''): string
    {
        $dsn = $runtime['dsn'] ?? '';
        $username = $runtime['username'] ?? '';
        if ($dsn || $username) {
            $key = "$dsn:$username";
        }
        return parent::setupId($runtime, $key);
    }

    public function run(array $runtime = [])
    {
        $access = $this->find('access');
        $sql = $runtime['sql'] ?? '';
        $params = $runtime['params'] ?? [];
        if (!$sql) {
            throw new RuntimeException('sql');
        }

        if ($sql === self::START || $sql === self::CLOSE) {
            return $this->transact($sql);
        }

        $statement = $access->prepare($sql);
        if (!$statement || !$statement->execute($params)) {
            $error = $statement->errorInfo();
            return [
                'status' => self::FAILED,
                'result' => [
                    'code' => $error[1],
                    'message' => $error[2]
                ]
            ];
        }

        $method = $runtime['method'] ?? self::$request->method ?? self::GET;
        $reslut = self::RUN_SUCCEEDED;
        if ($method === self::GET) {
            $reslut['result']['records'] = $statement->fetchAll(\PDO::FETCH_ASSOC);
            return $reslut;
        }
        if ($method === self::POST) {
            $reslut['result']['id'] = $access->lastInsertId('id');
            return $reslut;
        }
        $reslut['result']['count'] = $statement->rowCount();
        return $reslut;
    }

    public function driver()
    {
        $access = $this->find('access');
        return $access->getAttribute(\PDO::ATTR_DRIVER_NAME);
    }

    public function quote($value)
    {
        $access = $this->find('access');
        if (is_array($value)) {
            $quoted = $value;
            array_walk($quoted, $access->quote);
        } else {
            $quoted = $access->quote($value);
        }
        return $quoted ?: $value;
    }

    protected function init()
    {
        if (!extension_loaded(static::EXTENSION)) {
            throw new ExtensionException(static::EXTENSION);
        }

        $dsn = $this->find('dsn');
        if (!$dsn) {
            throw new RuntimeException('dsn');
        }
        $username = $this->find('username');
        if (!$username) {
            throw new RuntimeException('username');
        }
        $password = $this->find('password');

        try {
            $this->runtime['access'] = new \PDO($dsn, $username, $password);
        } catch (\PDOException $e) {
            throw new ResourceException(params: $dsn, previous: $e);
        }
    }

    protected function transact(string $action)
    {
        $access = $this->find('access');
        if ($action === self::START) {
            $status = $access->inTransaction() || $access->beginTransaction() ? self::SUCCEEDED : self:: FAILED;
            return ['status' => $status];
        }

        if ($action === self::CLOSE) {
            if (!$access->inTransaction()) return self::RUN_SUCCEEDED;
            if ($access->errorCode()) {
                $error = $access->errorInfo();
                return [
                    'status' => $access->rollBack() ? self::INCOMPLETE : self:: FAILED,
                    'result' => [
                        'code' => $error[1],
                        'message' => $error[2],
                    ]
                ];
            }
            return ['status' => $access->commit() ? self::SUCCEEDED : self:: FAILED];
        }

        return self::RUN_FAILED;
    }
}
